/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ArkFile, AstTreeUtils, ts} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData, FileMatcher, MatcherCallback, MatcherTypes, Utils} from '../../Index';
import {Defects} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {RuleListUtil} from "../../utils/common/DefectsList";
import {IssueReport} from '../../model/Defects';
import {RuleFix} from "../../model/Fix";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'AwaitThenableCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/lines-between-class-members-check.md",
    description: "Expected blank line between class members.",
};

type Options = [{
    exceptAfterOverload: boolean,
}]

export class LinesBetweenClassMembersCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private issueMap: Map<string, IssueReport> = new Map();
    private exceptAfterOverload = true;
    private errors: string[] = [];

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatcherCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        };
        return [fileMatcherCb];
    }

    public check = (arkFile: ArkFile) => {
        if (this.rule && this.rule.option) {
            const option = this.rule.option as Options;
            if (option.length > 0) {
                this.exceptAfterOverload = option[0].exceptAfterOverload;
            }
        }
        const code = arkFile.getCode();
        if (!code) {
            return;
        }
        const asRoot = AstTreeUtils.getASTNode(arkFile.getName(), code);
        const sourceFileObject = ts.getParseTreeNode(asRoot);
        if (sourceFileObject == undefined) {
            return;
        }

        this.loopNode(arkFile, asRoot, sourceFileObject);

        this.reportSortedIssues();
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFile, aNode: ts.Node) {
        const children = aNode.getChildren();
        for (const child of children) {
            if (ts.isClassDeclaration(child)) {
                this.checkClassMembers(child, sourceFile, targetFile);
            }
            this.loopNode(targetFile, sourceFile, child);
        }
    }

    private checkClassMembers(child: ts.ClassDeclaration, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        for (const modifier of child.modifiers ?? []) {
            if (modifier.kind === ts.SyntaxKind.AbstractKeyword) {
                return;
            }
        }
        if (child.members.length > 1) {
            for (let i = 0; i < child.members.length - 1; i++) {
                const currentMember = child.members[i];
                const nextMember = child.members[i + 1];
                const currentEndLine = getEndLine(currentMember);
                const nextStartLine = getStartLine(nextMember);
                if (this.exceptAfterOverload && isOverload(currentMember)) {
                    continue;
                }
                if (this.exceptAfterOverload) {
                    for (let j = i + 1; j < child.members.length; j++) {
                        if (checkMembersCondition(child, i, j)) {
                            let member = child.members[i];
                            if (member) {
                                let nextMember = child.members[j];
                                let positionInfo = this.getPositionInfo(nextMember, sourceFile);
                                let fix: RuleFix | undefined;
                                fix = {
                                    range: [currentMember.getStart(), currentMember.getEnd()],
                                    text: currentMember.getText() + '\n'
                                };
                                const defect = this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description, fix);
                                this.issueMap.set(defect.fixKey, {defect, fix});
                            }
                            break;
                        }
                    }
                } else {
                    if (nextStartLine - currentEndLine <= 1) {
                        this.errors.push(`Expected blank line between class members at line ${currentEndLine + 1}`);
                        let positionInfo = this.getPositionInfo(nextMember, sourceFile);
                        let fix: RuleFix | undefined;
                        fix = {
                            range: [currentMember.getStart(), currentMember.getEnd()],
                            text: currentMember.getText() + '\n'
                        };
                        const defect = this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description, fix);
                        this.issueMap.set(defect.fixKey, {defect, fix});
                    }
                }
            }
        }

        function checkMembersCondition(child: ts.ClassDeclaration, i: number, offX: number) {
            if (child.members[offX]) {
                if (getStartLine(child.members[offX]) - getEndLine(child.members[i]) <= offX - i) {
                    return !isOverload(child.members[offX]);
                } else {
                    return false;
                }
            }
            return false;
        }

        function getStartLine(node: ts.Node): number {
            const start = node.getStart(sourceFile);
            return sourceFile.getLineAndCharacterOfPosition(start).line + 1;
        }

        function getEndLine(node: ts.Node): number {
            const end = node.getEnd();
            return sourceFile.getLineAndCharacterOfPosition(end).line + 1;
        }

        function isOverload(node: ts.ClassElement): boolean {
            return node.kind === ts.SyntaxKind.MethodDeclaration && (node as ts.MethodDeclaration).body === undefined;
        }
    }

    private getPositionInfo(expression: ts.ClassElement, sourceFile: ts.SourceFileLike) {
        const start = expression.getStart();
        const end = expression.getEnd();
        const startPositionInfo = sourceFile.getLineAndCharacterOfPosition(start);
        const endPositionInfo = sourceFile.getLineAndCharacterOfPosition(end);
        return {
            startPosition: startPositionInfo,
            endPosition: endPositionInfo
        };
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string, fix?: RuleFix) {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const fixKey = line + '%' + startCol + '%' + this.rule.ruleId;
        const mergeKey = filePath + '%' + fixKey + '%' + this.metaData.description;
        const defect = new Defects(line, startCol, message, severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey);
        this.issues.push(new IssueReport(defect, fix));
        RuleListUtil.push(defect);
        return defect;
    }

    private reportSortedIssues(): void {
        if (this.issueMap.size === 0) {
            return;
        }
        const sortedIssues = Array.from(this.issueMap.entries())
            .sort(([keyA], [keyB]) => Utils.sortByLineAndColumn(keyA, keyB));
        this.issues = [];
        sortedIssues.forEach(([_, issue]) => {
            RuleListUtil.push(issue.defect);
            this.issues.push(issue);
        });
    }
}